package regsrv;

import java.rmi.NoSuchObjectException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;


import common.DuplicateException;
import common.PlayerIntf;
import common.RegistrationServerIntf;

/**
 * Il Registration Server implementando {@link RegistrationServerIntf}. Si 
 * rimanda a {@link RegistrationServerIntf} per ulteriori dettagli sul 
 * Registration Server.
 * Si occupa inoltre della creazione di un RMI Registry locale, su cui viene
 * fatto il bind.
 */
public class RegistrationServer implements RegistrationServerIntf {

	/**
	 * Disabilita il timer (per debug)
	 */
	private boolean enable= true;
	
	/*
	 * TODO: invece che usare metodi synchronized, si potrebbe migliorare
	 * l'implementazione utilizzando dei blocchi synchronized sulla singola
	 * mappa utilizzata (players oppure playGroups). E' vero anche che alcuni
	 * metodi utilizzano entrambe le mappe... quindi potrebbe non essere cosi'
	 * importante. 
	 */
	
	
	/**
	 * Mappa fra nickname (String) e oggetto remoto (PlayerIntf) dei player registrati
	 */
	private HashMap<String, PlayerIntf> players;
	
	/**
	 * Mappa fra nomi di gruppi di gioco (String) e oggetto remoto (PlayerIntf) del player proprietario
	 */
	private HashMap<String, PlayerIntf> playGroups;
	
	/**
	 * Timer per i gruppi.
	 * Mappa da nome gruppo al corrispondente timer
	 */
	private HashMap<String, LeaseTimer> groupLeases;
	
	/**
	 * Timer per i player.
	 * Mappa da nome del player al corrispondente timer
	 */
	private HashMap<String, LeaseTimer> playerLeases;
	
	/**
	 * Timer globale: gestisce i timer di tutti i gruppi e di tutti i player
	 */
	private ScheduledThreadPoolExecutor globalTimer;
	
	/** 
	 * Avvia RMI Registry sulla porta di default (1099) ed avvia il 
	 * Registration Server.
	 * Non necessita di argomenti.
	 * 
	 * Se avviato da linea di comando:
	 * java -Djava.security.policy=../security.policy cluedo/regsrv/RegistrationServer
	 * 
	 * Se si utilizzano i "Launch configuration" di Eclipse, aggiungere il 
	 * seguente argomento per la VM:
	 * -Djava.security.policy=security.policy
	 */
	public static void main(String[] args){
		RegistrationServerIntf stub= null;
		RegistrationServer regSrv;
		Registry registry;
		Scanner scan;
		boolean h, exported;

		exported= false;
		
		// security manager
        if (System.getSecurityManager() == null) {
        	System.out.println("Installazione Security Manager");
            System.setSecurityManager(new SecurityManager());
        } else {
        	System.out.println("Security Manager gia' installato");
        }
        
        // avvio di rmiregistry
        try {
            LocateRegistry.createRegistry(1099); 
            System.out.println("Java RMI registry creato");
        } catch (RemoteException e) {
            //do nothing, error means registry already exists
            System.out.println("Java RMI registry gia' esistente");
        }
        
        // creazione istanza del Registration Server
        regSrv= new RegistrationServer();
        
        try {
	        // esportazione
        	System.out.println("Esportazione");
	 		stub = (RegistrationServerIntf)	UnicastRemoteObject.exportObject(regSrv, 0);
	 		exported= true;
			
			// rebind
	    	System.out.println("Rebind di " + REGSRV);
	    	registry = LocateRegistry.getRegistry("localhost");
			registry.rebind(REGSRV, stub);
	
			System.out.println("Registration Server pronto!");
			
			// premi invio per terminare
			scan= new Scanner(System.in);
			System.out.println("Premi invio in qualsiasi momento per terminare il Registration Server.");
			System.out.println();
			scan.nextLine();
			System.out.println("Terminazione del Registration Server");
			
			// unbind
			System.out.println("Unbind di " + REGSRV);
			registry.unbind(REGSRV);
			
			// unexport
			System.out.println("Rimozione esportazione (forzata)");
			h= UnicastRemoteObject.unexportObject(regSrv, true);
			if (h) {
				System.out.println("UnicastRemoteObject(regSrv, true) ha restituito " + h +
						": operazione eseguita con successo");
			} else {
				System.err.println("UnicastRemoteObject(regSrv, true) ha restituito " + h +
						": operazione non eseguita perche' sono presenti chiamate pendenti");
			}
        } catch (RemoteException e) {
        	e.printStackTrace();
        	// se non e' stato fatto unexport lo faccio per far terminare il processo
        	if (exported==true) {
        		System.out.println("Terminazione anormale ma oggetto remoto ancora esportato: rimuovo l'esportazione");
        		try {
					UnicastRemoteObject.unexportObject(regSrv, true);
				} catch (NoSuchObjectException e1) {
					e1.printStackTrace();
				}
        	}
        } catch (NotBoundException e) {
			e.printStackTrace();
		}
        
	}

	/**
	 * Crea una nuova istanza del Registration Server.
	 * Non fa export o bind di esso.
	 */
	public RegistrationServer() {
		if (!enable) System.out.println("ATTENZIONE: TIMER NON ABILITATO");
		
		players= new HashMap<String, PlayerIntf>();
		playGroups= new HashMap<String, PlayerIntf>();
		
		// timer
		playerLeases= new HashMap<String, LeaseTimer>();
		groupLeases= new HashMap<String, LeaseTimer>();
		globalTimer= new ScheduledThreadPoolExecutor(1);
		globalTimer.setRemoveOnCancelPolicy(true);
	}
    
	@Override
	public synchronized Map<String, PlayerIntf> addRegistration(PlayerIntf p) 
			throws DuplicateException, RemoteException {
		String name= p.getNickname();
		
		if (players.containsKey(name) || players.containsValue(p)) {
			// player gia' presente
			System.err.println("Registrazione di \"" + name + "\"... player gia' presente: operazione annullata");
			throw new DuplicateException("Il nickname \"" + name + "\" e' gia' stato utilizzato");
		} else {
			// aggiunto il player
			System.out.println("Registrazione di \"" + name + "\"...");
			players.put(name, p);
			System.out.println("Players registrati: " + players.keySet());
			
			// timer
			LeaseTimer lt= new LeaseTimer(name, LeaseTimer.PLAYER);
			playerLeases.put(name, lt);

			// non devo notificare nessuno del nuovo utente

			// restituisco l'attuale insieme dei proprietari di gruppi
			return playGroups;
		}
	}

	@Override
	public synchronized void delRegistration(String name) {
		System.out.println("Cancellazione registrazione di \"" + name + "\"");
		players.remove(name);
		System.out.println("Players registrati: " + players.keySet());
		
		// rimozione timer
		if (playerLeases.containsKey(name)) {
			LeaseTimer lt= playerLeases.get(name);
			lt.stop();
			playerLeases.remove(name);
		}
		
		// non devo notificare nessuno di cio'
	}

	@Override
	public synchronized void addPlayGroup(PlayerIntf p, String name)
			throws DuplicateException {
		if (playGroups.containsKey(name)) {
			// nome di gruppo di gioco gia' presente
			System.err.println("Creazione gruppo di gioco: nome \"" + name + "\" gia' usato");
			throw new DuplicateException("Esiste gia' un gruppo di gioco con nome \"" + name + "\"");
		} else if (playGroups.containsValue(p)) {
			// il player e' gia' proprietario di un gruppo di gioco
			String pName;
			try {
				pName= p.getNickname();
			} catch (RemoteException e) {
				pName= "<nome non recuperabile>";
			}
			
			System.err.println("Creazione gruppo di gioco: il player \"" + pName 
					+ "\" e' gia' proprietario di un altro gruppo");
			throw new DuplicateException("Il player e' gia' proprietario di un altro gruppo");
		} else {
			// aggiungo il nuovo gruppo di gioco
			System.out.println("Creazione gruppo di gioco \"" + name + "\"...");
			playGroups.put(name, p);
			System.out.println("Gruppi registrati: " + playGroups.keySet());
			
			// aggiungo il timer
			LeaseTimer lt= new LeaseTimer(name, LeaseTimer.GROUP);
			groupLeases.put(name, lt);

			// devo notificare tutti i player registrati
			for (Map.Entry<String, PlayerIntf> entry : players.entrySet()) {
				try {
					entry.getValue().notifyAddPlayGroup(name, p);
				} catch (RemoteException e) {
					System.err.println("Invio notifica aggiunta gruppo \"" + name + "\" al player \"" + entry.getKey() + "\" non riuscita.");
				}
			}
		}
	}

	@Override
	public synchronized void delPlayGroup(String name) {
		System.out.println("Cancellazione gruppo di gioco \"" + name + "\"");
		playGroups.remove(name);
		System.out.println("Gruppi registrati: " + playGroups.keySet());


		// rimozione timer
		if (groupLeases.containsKey(name)) {
			LeaseTimer lt= groupLeases.get(name);
			lt.stop();
			groupLeases.remove(name);
		}
		
		// devo notificare tutti i player registrati
		for (Map.Entry<String, PlayerIntf> entry : players.entrySet()) {
			try {
				entry.getValue().notifyDelPlayGroup(name);
			} catch (RemoteException e) {
				System.err.println("Invio notifica rimozione gruppo \"" + name + "\" al player \"" + entry.getKey() + "\" non riuscita.");
			}
		}		
	}

	@Override
	public synchronized void changePlayGroupOwner(PlayerIntf newOwner, String groupName) {
		
		System.out.println("Cambiato il proprietario del gruppo \"" + groupName + "\"");
		playGroups.put(groupName, newOwner);
		
		// rinfresco automaticamente il timer
		refreshPlayGroup(groupName);
		
		// devo notificare tutti i player registrati
		for (Map.Entry<String, PlayerIntf> entry : players.entrySet()) {
			try {
				entry.getValue().notifyChangePlayGroupOwner(groupName, newOwner);
			} catch (RemoteException e) {
				System.err.println("Invio notifica cambio proprietario del gruppo " + groupName + " al player \"" + entry.getKey() + "\" non riuscita.");
			}
		}
		
	}
	
	@Override
	public synchronized boolean refreshPlayGroup(String name) {
		if (groupLeases.containsKey(name)) {
			LeaseTimer lt= groupLeases.get(name);
			lt.restart();
			
			return true;
		} else {
			return false;
		}
	}

	@Override
	public synchronized boolean refreshPlayer(String name) {
		if (playerLeases.containsKey(name)) {
			LeaseTimer lt= playerLeases.get(name);
			lt.restart();
			
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Gestisce il timer relativo ad un gruppo o un player
	 */
	private class LeaseTimer implements Runnable {
		/**
		 * Identificativo per i gruppi
		 */
		static final int GROUP= 0;
		/**
		 * Identificativo per i player
		 */
		static final int PLAYER= 1;
		
		/**
		 * <code>GROUP</code> o <code>PLAYER</code>
		 */
		private int type;
		
		/**
		 * Nome del gruppo/player
		 */
		private String name;

		/**
		 * Usato per cancellare il timer
		 */
		private ScheduledFuture<?> sf;
		
		/**
		 * Periodo di tutti i timer
		 */
		private final long period= 10; // in secondi
		
		/**
		 * Crea un nuovo LeaseTimer
		 * @param name
		 * @param type deve essere <code>GROUP</code> o <code>PLAYER</code>
		 */
		public LeaseTimer(String name, int type) {
			this.name= name;
			if (type!=GROUP && type!=PLAYER) {
				throw new RuntimeException("Il tipo puo' essere solo GROUP o PLAYER");
			}
			this.type= type;
			sf= globalTimer.scheduleAtFixedRate(this, period, period, TimeUnit.SECONDS);
		}
		
		/**
		 * Ferma l'esecuzione di questo timer
		 */
		public void stop() {
			stop(false);
		}
		
		/**
		 * Ferma l'esecuzione di questo timer
		 * @param silent se true non viene stampato niente su stderr se il timer non puo' essere fermato
		 */
		private void stop(boolean silent) {
			boolean b= sf.cancel(false);
			
			if (!b && !silent) {
				String str1;
				if (type==GROUP) str1= "Timer gruppo ";
				else str1= "Timer player ";
				System.err.println(str1 + "\"" + name + "\": il timer non puo' essere fermato (restituito " + b + ")");
			}
		}
		
		/**
		 * Ferma il timer e fa ricominciare il conteggio del tempo
		 */
		public void restart() {
			//System.out.println("Refresh " + this);
			stop();
			sf= globalTimer.scheduleAtFixedRate(this, period, period, TimeUnit.SECONDS);
		}
		
		@Override
		public void run() {
			if (!enable) return;
			System.out.println("Rilevato crash: timeout " + this);
			//stop(true);
			
			if (type==GROUP) {
				delPlayGroup(name);
			} else {
				delRegistration(name);
			}
		}
		
		@Override
		public String toString() {
			if (type==GROUP) {
				return "timer gruppo \"" + name + "\"";
			} else {
				return "timer player \"" + name + "\"";
			}
		}
	}
}
